10.6 StreamTokenizer

尽管StreamTokenizer并不是从InputStreamOutputStream派生的,但它只随同InputStream工作,所以十分恰当地包括在库的IO部分中。

StreamTokenizer类用于将任何InputStream分割为一系列“记号”(Token)。这些记号实际是一些断续的文本块,中间用我们选择的任何东西分隔。例如,我们的记号可以是单词,中间用空白(空格)以及标点符号分隔。 下面是一个简单的程序,用于计算各个单词在文本文件中重复出现的次数:

  1. //: SortedWordCount.java
  2. // Counts words in a file, outputs
  3. // results in sorted form.
  4. import java.io.*;
  5. import java.util.*;
  6. import c08.*; // Contains StrSortVector
  7. class Counter {
  8. private int i = 1;
  9. int read() { return i; }
  10. void increment() { i++; }
  11. }
  12. public class SortedWordCount {
  13. private FileInputStream file;
  14. private StreamTokenizer st;
  15. private Hashtable counts = new Hashtable();
  16. SortedWordCount(String filename)
  17. throws FileNotFoundException {
  18. try {
  19. file = new FileInputStream(filename);
  20. st = new StreamTokenizer(file);
  21. st.ordinaryChar('.');
  22. st.ordinaryChar('-');
  23. } catch(FileNotFoundException e) {
  24. System.out.println(
  25. "Could not open " + filename);
  26. throw e;
  27. }
  28. }
  29. void cleanup() {
  30. try {
  31. file.close();
  32. } catch(IOException e) {
  33. System.out.println(
  34. "file.close() unsuccessful");
  35. }
  36. }
  37. void countWords() {
  38. try {
  39. while(st.nextToken() !=
  40. StreamTokenizer.TT_EOF) {
  41. String s;
  42. switch(st.ttype) {
  43. case StreamTokenizer.TT_EOL:
  44. s = new String("EOL");
  45. break;
  46. case StreamTokenizer.TT_NUMBER:
  47. s = Double.toString(st.nval);
  48. break;
  49. case StreamTokenizer.TT_WORD:
  50. s = st.sval; // Already a String
  51. break;
  52. default: // single character in ttype
  53. s = String.valueOf((char)st.ttype);
  54. }
  55. if(counts.containsKey(s))
  56. ((Counter)counts.get(s)).increment();
  57. else
  58. counts.put(s, new Counter());
  59. }
  60. } catch(IOException e) {
  61. System.out.println(
  62. "st.nextToken() unsuccessful");
  63. }
  64. }
  65. Enumeration values() {
  66. return counts.elements();
  67. }
  68. Enumeration keys() { return counts.keys(); }
  69. Counter getCounter(String s) {
  70. return (Counter)counts.get(s);
  71. }
  72. Enumeration sortedKeys() {
  73. Enumeration e = counts.keys();
  74. StrSortVector sv = new StrSortVector();
  75. while(e.hasMoreElements())
  76. sv.addElement((String)e.nextElement());
  77. // This call forces a sort:
  78. return sv.elements();
  79. }
  80. public static void main(String[] args) {
  81. try {
  82. SortedWordCount wc =
  83. new SortedWordCount(args[0]);
  84. wc.countWords();
  85. Enumeration keys = wc.sortedKeys();
  86. while(keys.hasMoreElements()) {
  87. String key = (String)keys.nextElement();
  88. System.out.println(key + ": "
  89. + wc.getCounter(key).read());
  90. }
  91. wc.cleanup();
  92. } catch(Exception e) {
  93. e.printStackTrace();
  94. }
  95. }
  96. } ///:~

最好将结果按排序格式输出,但由于Java 1.0和Java 1.1都没有提供任何排序方法,所以必须由自己动手。这个目标可用一个StrSortVector方便地达成(创建于第8章,属于那一章创建的软件包的一部分。记住本书所有子目录的起始目录都必须位于类路径中,否则程序将不能正确地编译)。

为打开文件,使用了一个FileInputStream。而且为了将文件转换成单词,从FileInputStream中创建了一个StreamTokenizer。在StreamTokenizer中,存在一个默认的分隔符列表,我们可用一系列方法加入更多的分隔符。在这里,我们用ordinaryChar()指出“该字符没有特别重要的意义”,所以解析器不会把它当作自己创建的任何单词的一部分。例如,st.ordinaryChar('.')表示小数点不会成为解析出来的单词的一部分。在与Java配套提供的联机文档中,可以找到更多的相关信息。

countWords()中,每次从数据流中取出一个记号,而ttype信息的作用是判断对每个记号采取什么操作——因为记号可能代表一个行尾、一个数字、一个字符串或者一个字符。

找到一个记号后,会查询Hashtable counts,核实其中是否已经以“键”(Key)的形式包含了一个记号。若答案是肯定的,对应的Counter(计数器)对象就会自增,指出已找到该单词的另一个实例。若答案为否,则新建一个Counter——因为Counter构造器会将它的值初始化为1,正是我们计算单词数量时的要求。

SortedWordCount并不属于Hashtable(散列表)的一种类型,所以它不会继承。它执行的一种特定类型的操作,所以尽管keys()values()方法都必须重新揭示出来,但仍不表示应使用那个继承,因为大量Hashtable方法在这里都是不适当的。除此以外,对于另一些方法来说(比如getCounter()——用于获得一个特定字符串的计数器;又如sortedKeys()——用于产生一个枚举),它们最终都改变了SortedWordCount接口的形式。

main()内,我们用SortedWordCount打开和计算文件中的单词数量——总共只用了两行代码。随后,我们为一个排好序的键(单词)列表提取出一个枚举。并用它获得每个键以及相关的Count(计数)。注意必须调用cleanup(),否则文件不能正常关闭。 采用了StreamTokenizer的第二个例子将在第17章提供。

10.6.1 StringTokenizer

尽管并不必要IO库的一部分,但StringTokenizer提供了与StreamTokenizer极相似的功能,所以在这里一并讲述。

StringTokenizer的作用是每次返回字符串内的一个记号。这些记号是一些由制表站、空格以及新行分隔的连续字符。因此,字符串"Where is my cat?"的记号分别是"Where""is""my""cat?"。与StreamTokenizer类似,我们可以指示StringTokenizer按照我们的愿望分割输入。但对于StringTokenizer,却需要向构造器传递另一个参数,即我们想使用的分隔字符串。通常,如果想进行更复杂的操作,应使用StreamTokenizer

可用nextToken()StringTokenizer对象请求字符串内的下一个记号。该方法要么返回一个记号,要么返回一个空字符串(表示没有记号剩下)。

作为一个例子,下述程序将执行一个有限的句法分析,查询键短语序列,了解句子暗示的是快乐亦或悲伤的含义。

  1. //: AnalyzeSentence.java
  2. // Look for particular sequences
  3. // within sentences.
  4. import java.util.*;
  5. public class AnalyzeSentence {
  6. public static void main(String[] args) {
  7. analyze("I am happy about this");
  8. analyze("I am not happy about this");
  9. analyze("I am not! I am happy");
  10. analyze("I am sad about this");
  11. analyze("I am not sad about this");
  12. analyze("I am not! I am sad");
  13. analyze("Are you happy about this?");
  14. analyze("Are you sad about this?");
  15. analyze("It's you! I am happy");
  16. analyze("It's you! I am sad");
  17. }
  18. static StringTokenizer st;
  19. static void analyze(String s) {
  20. prt("\nnew sentence >> " + s);
  21. boolean sad = false;
  22. st = new StringTokenizer(s);
  23. while (st.hasMoreTokens()) {
  24. String token = next();
  25. // Look until you find one of the
  26. // two starting tokens:
  27. if(!token.equals("I") &&
  28. !token.equals("Are"))
  29. continue; // Top of while loop
  30. if(token.equals("I")) {
  31. String tk2 = next();
  32. if(!tk2.equals("am")) // Must be after I
  33. break; // Out of while loop
  34. else {
  35. String tk3 = next();
  36. if(tk3.equals("sad")) {
  37. sad = true;
  38. break; // Out of while loop
  39. }
  40. if (tk3.equals("not")) {
  41. String tk4 = next();
  42. if(tk4.equals("sad"))
  43. break; // Leave sad false
  44. if(tk4.equals("happy")) {
  45. sad = true;
  46. break;
  47. }
  48. }
  49. }
  50. }
  51. if(token.equals("Are")) {
  52. String tk2 = next();
  53. if(!tk2.equals("you"))
  54. break; // Must be after Are
  55. String tk3 = next();
  56. if(tk3.equals("sad"))
  57. sad = true;
  58. break; // Out of while loop
  59. }
  60. }
  61. if(sad) prt("Sad detected");
  62. }
  63. static String next() {
  64. if(st.hasMoreTokens()) {
  65. String s = st.nextToken();
  66. prt(s);
  67. return s;
  68. }
  69. else
  70. return "";
  71. }
  72. static void prt(String s) {
  73. System.out.println(s);
  74. }
  75. } ///:~

对于准备分析的每个字符串,我们进入一个while循环,并将记号从那个字符串中取出。请注意第一个if语句,假如记号既不是"I",也不是"Are",就会执行continue(返回循环起点,再一次开始)。这意味着除非发现一个"I"或者"Are",才会真正得到记号。大家可能想用==代替equals()方法,但那样做会出现不正常的表现,因为==比较的是引用值,而equals()比较的是内容。

analyze()方法剩余部分的逻辑是搜索"I am sad"(我很忧伤、"I am nothappy"(我不快乐)或者"Are you sad?"(你悲伤吗?)这样的句法格式。若没有break语句,这方面的代码甚至可能更加散乱。大家应注意对一个典型的解析器来说,通常都有这些记号的一个表格,并能在读取新记号的时候用一小段代码在表格内移动。

无论如何,只应将StringTokenizer看作StreamTokenizer一种简单而且特殊的简化形式。然而,如果有一个字符串需要进行记号处理,而且StringTokenizer的功能实在有限,那么应该做的全部事情就是用StringBufferInputStream将其转换到一个数据流里,再用它创建一个功能更强大的StreamTokenizer